package sn.radius.client

import org.aaa4j.radius.client.{RadiusClient, RadiusClientException}
import org.aaa4j.radius.client.clients.UdpRadiusClient
import org.aaa4j.radius.core.attribute.{Attribute, StringData, TextData}
import org.aaa4j.radius.core.attribute.attributes.{NasIdentifier, UserName, UserPassword}
import org.aaa4j.radius.core.packet.Packet
import org.aaa4j.radius.core.packet.packets.{AccessAccept, AccessChallenge, AccessReject, AccessRequest}

import java.net.InetSocketAddress
import java.nio.charset.StandardCharsets.UTF_8
import scala.io.{Source, StdIn}
import scala.jdk.CollectionConverters.*

import PacketUtils.*

/**Build a RadiusClient using UdpRadiusClient.newBuilder() and send a request packet using send()*/
object Main {

  //private val radiusSecret = "radsec"
  private val useOtp = true
  
/*
  val username_jbrown = "jbrown"
  val username_bwilson = "bwilson"
  //val username = "john.doe"
  val username_r_alice = "r_alice"
  //val username = "alice" // user from `myrealm`
  val username_r_alice_otp = "r_alice_otp"
  val username_nick = "nick"
  //val username = username_nick
  //val username = "aaa_bob_otp"
  //val username = "nick2"

  //val password = "hunter2"
  //val password = "r_alice"
  val password_alice = "alice"
  val password_r_alice = "r_alice"
  val password_r_alice_otp = "secret"
  val password_jbrown = "password"
  val password_bwilson = "password"
  val password_nick = "nick"
  //val password = password_nick
  //val password = "bob"
  //val password = "nick2"
*/
  private val helpText =
    """Usage: java -jar <jar-name> {option=value}
      | Options:
      |   --help            -- this text
      |   --host            -- host (default 127.0.0.1)
      |   --otp             -- OTP
      |   --password        -- user password
      |   --port            -- UDP port (default 1812)
      |   --radius          -- RADIUS shared secret
      |   --realm           -- realm name
      |   --user            -- username
      | Example:
      |   java -jar sn-radius-client-assembly-0.2.0.jar --radius=radsec --realm=my_realm --user=nick --password=secret --otp=123456
      |""".stripMargin

  case class RadiusParams(host: String, port: Int, radiusSecret: String, realm: String, user: String, password: String, otp: String) {
    def isValid: Boolean = host != null && port != 0 && radiusSecret != null && realm != null && user != null && password != null && otp != null
  }

  def main(args: Array[String]): Unit = {
    if args.contains("--help") || args.contains("-h") then
      println(helpText)
    else
      processArguments(args) match {
        case Left(err) => throw new IllegalArgumentException(err)
        case Right(data) => run(data)
      }

  }

  def processArguments(args: Array[String]): Either[String, RadiusParams] = {
    var ans: Either[String, RadiusParams] = Right(RadiusParams("127.0.0.1", 1812, null, null, null, null, null))
    for (arg <- args) {
      val arr = arg.split("=", 2)
      if (arr.length != 2) ans = Left(s"Wrong argument $arg")
      else arr(0) match {
        case "--otp"      if ans.isRight => ans = ans.map(a => a.copy(otp = arr(1)))
        case "--host"     if ans.isRight => ans = ans.map(a => a.copy(host = arr(1)))
        case "--port"     if ans.isRight => ans = ans.map(a => a.copy(port = arr(1).toInt))
        case "--password" if ans.isRight => ans = ans.map(a => a.copy(password = arr(1)))
        case "--radius"   if ans.isRight => ans = ans.map(a => a.copy(radiusSecret = arr(1)))
        case "--realm"    if ans.isRight => ans = ans.map(a => a.copy(realm = arr(1)))
        case "--user"     if ans.isRight => ans = ans.map(a => a.copy(user = arr(1)))
        case x if ans.isRight => ans = Left(s"Unrecognized argument $x")
        case _ => // already error
      }
    }

    if ans.forall(_.isValid) then ans
    else Left(s"Not all mandatory arguments specified: $ans")
  }

  def run(cfg: RadiusParams): Unit = {
    println(s"Run with $cfg")

    val radiusClient: UdpRadiusClient = UdpRadiusClient.newBuilder()
      .secret(cfg.radiusSecret.getBytes(UTF_8))
      .address(new InetSocketAddress(cfg.host, cfg.port))
      .build()

    val user = makeUsername(cfg.realm, cfg.user)
    val pswd = makePassword(cfg.password, cfg.otp)
    val accessRequest = new AccessRequest(List(
      new UserName(new TextData(user)),
      new UserPassword(new StringData(pswd.getBytes(UTF_8))),
      new NasIdentifier(new TextData("SSID1")) // An Access-Request MUST contain either a NAS-IP-Address or a NAS-Identifier (or both).
    ).asJava)

    try {
      println(s"Try to authorize `$user` with `$pswd`")
      val responsePacket: Packet = radiusClient.send(accessRequest)

      responsePacket match
        case a: AccessAccept =>
          println("Accepted")
          //println(a.text)
        case challenge: AccessChallenge =>
          println(s"AccessChallenge: $challenge")
          //println(challenge.text)
          print("Enter OTP: ")
          val otp = StdIn.readLine()
          assert(otp.length == 6)
          val challengeAnswer = new AccessRequest(List(
            // not implemented and never will be
            new UserName(new TextData(user)),
            new UserPassword(new StringData(otp.getBytes(UTF_8))),
            new NasIdentifier(new TextData("SSID1")) // An Access-Request MUST contain either a NAS-IP-Address or a NAS-Identifier (or both).
          ).asJava)
          println(s"Try to authorize $user with 2nd factor")
          val challengeResponse: Packet = radiusClient.send(challengeAnswer)
          println(s"challengeResponse = $challengeResponse")
        case r: AccessReject =>
          println(s"Rejected")
          //println(r.text)
        case x => println(s"Not implemented response $x")
    }
    catch {
      case e: RadiusClientException => e.printStackTrace()
    }
  }

  private def makeUsername(realm: String, user: String): String = s"$realm/$user"
  private def makePassword(pass: String, otp: String): String = s"$pass$otp"

  /*
  private def enterData(args: Array[String]): Either[String, (String, String, String, String, String)] = {
    print(s"realm: ")
    val realm = StdIn.readLine()

    print(s"username: ")
    val user = StdIn.readLine()

    print(s"password: ")
    val pass = StdIn.readLine()
    //assert(pass.length <= 10, "Bug in the AAA library. Password length must be <= 10.")

    print(s"OTP for $user: ")
    val otp = StdIn.readLine()
    //assert(otp.length == 6)

    (realm, user, pass, otp)
  }
  */

  //  private def appendOtpPassword(): String = {
  //    print(s"OTP for $username: ")
  //    val otp = StdIn.readLine()
  //    assert(otp.length == 6)
  //    //otp + password
  //    password + otp
  //  }
}
